{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "86a4bd36",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "下面展示基于隐马尔科夫模型的序列标注监督学习的代码。这里以命名实体识别任务为例，所使用的数据是Books数据集。为简单起见，标签序列采用BIO格式。首先构建数据集和标签集合："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "41673bbb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'B-NAME': 1, 'I-ORG': 2, 'B-PRO': 3, 'B-EDU': 4, 'I-NAME': 5, 'B-LOC': 6, 'B-TITLE': 7, 'B-RACE': 8, 'I-TITLE': 9, 'I-RACE': 10, 'I-PRO': 11, 'B-CONT': 12, 'I-EDU': 13, 'I-LOC': 14, 'I-CONT': 15, 'B-ORG': 16, 'O': 0}\n"
     ]
    }
   ],
   "source": [
    "import os\n",
    "import json\n",
    "from collections import defaultdict\n",
    "\n",
    "\n",
    "class NERDataset:\n",
    "    def __init__(self):\n",
    "        train_file, test_file, label_file = 'ner_train.jsonl',\\\n",
    "            'ner_test.jsonl', 'ner_labels.json'\n",
    "        \n",
    "        def read_file(file_name):\n",
    "            with open(file_name, 'r', encoding='utf-8') as fin:\n",
    "                json_list = list(fin)\n",
    "            data_split = []\n",
    "            for json_str in json_list:\n",
    "                raw = json.loads(json_str)\n",
    "                d = {'tokens': raw['tokens'], 'tags': raw['tags']}\n",
    "                data_split.append(d)\n",
    "            return data_split\n",
    "        \n",
    "        # 读取JSON文件，转化为Python对象\n",
    "        self.train_data, self.test_data = read_file(train_file),\\\n",
    "            read_file(test_file)\n",
    "        self.label2id = json.loads(open(label_file, 'r').read())\n",
    "        self.id2label = {}\n",
    "        for k, v in self.label2id.items():\n",
    "            self.id2label[v] = k\n",
    "        print(self.label2id)\n",
    "\n",
    "    # 建立词表，过滤低频词\n",
    "    def build_vocab(self, min_freq=3):\n",
    "        # 统计词频\n",
    "        frequency = defaultdict(int)\n",
    "        for data in self.train_data:\n",
    "            tokens = data['tokens']\n",
    "            for token in tokens:\n",
    "                frequency[token] += 1\n",
    "                \n",
    "        print(f'unique tokens = {len(frequency)}, '+\\\n",
    "              f'total counts = {sum(frequency.values())}, '+\\\n",
    "              f'max freq = {max(frequency.values())}, '+\\\n",
    "              f'min freq = {min(frequency.values())}')\n",
    "        \n",
    "        # 由于词与标签一一对应，不能随便删除字符，\n",
    "        # 因此加入<unk>用于替代未登录词，加入<pad>用于批量训练\n",
    "        self.token2id = {'<pad>': 0, '<unk>': 1}\n",
    "        self.id2token = {0: '<pad>', 1: '<unk>'}\n",
    "        total_count = 0\n",
    "        # 将高频词加入词表\n",
    "        for token, freq in sorted(frequency.items(),\\\n",
    "                key=lambda x: -x[1]):\n",
    "            if freq > min_freq:\n",
    "                self.token2id[token] = len(self.token2id)\n",
    "                self.id2token[len(self.id2token)] = token\n",
    "                total_count += freq\n",
    "            else:\n",
    "                break\n",
    "        print(f'min_freq = {min_freq}, '\n",
    "              f'remaining tokens = {len(self.token2id)}, '\n",
    "              f'in-vocab rate = {total_count / sum(frequency.values())}')\n",
    "\n",
    "    # 将文本输入转化为词表中对应的索引\n",
    "    def convert_tokens_to_ids(self):\n",
    "        for data_split in [self.train_data, self.test_data]:\n",
    "            for data in data_split:\n",
    "                data['token_ids'] = []\n",
    "                for token in data['tokens']:\n",
    "                    data['token_ids'].append(self.token2id.get(token, 1)) \n",
    "        \n",
    "dataset = NERDataset()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "25650a7c",
   "metadata": {},
   "source": [
    "接下来建立词表，将词元转换为索引，并将数据转化为适合训练的格式。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "a3eb49e5",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "unique tokens = 2454, total counts = 182068, max freq = 5630, min freq = 1\n",
      "min_freq = 0, remaining tokens = 2456, in-vocab rate = 1.0\n",
      "[595, 510, 353, 263, 424, 225, 764, 637, 353, 65, 80, 47, 41, 74, 53, 41, 141, 23, 74, 7, 259, 27, 135, 47, 31, 74, 67, 25, 1605, 815, 1157, 225, 595, 1158, 738, 688, 353, 1025, 65, 234, 27, 135, 316, 41, 31, 7, 80, 41, 42, 31, 47, 27, 528, 41, 141, 27, 135, 67, 1005, 1606, 363, 19, 677, 294, 37, 678, 1756, 604, 873, 1006, 1005, 353, 33, 30, 9, 113, 5, 45, 11, 82, 60, 21, 26, 5, 186, 153, 100, 565, 873, 353, 424, 1993, 387, 346, 250, 13, 5, 102, 214, 25, 11, 82, 60, 21, 26, 5, 186, 2, 7, 459, 97, 89, 147, 140, 139, 69, 46, 21, 12, 2, 7, 964, 98, 240, 677, 59, 9, 103, 11, 82, 60, 335, 200, 76, 26, 6, 110, 20, 9, 15, 4, 122, 539, 241, 99, 621, 40, 200, 57, 82, 156, 25, 11, 82, 381, 371, 91, 202, 6, 52, 2, 7, 670, 100, 84, 204, 53, 120, 27, 42, 3, 56, 1159, 3, 329, 31, 265, 31, 3, 56, 394, 394, 3, 84, 357, 84, 7, 25, 397, 7, 41, 7, 74, 7, 135, 7, 31, 7, 87, 7, 259, 7, 31, 7, 74, 7, 41, 7, 131, 7, 28, 289, 385, 4]\n",
      "['阿', '里', '斯', '提', '德', '·', '波', '拉', '斯', '（', 'A', 'r', 'i', 's', 't', 'i', 'd', 'e', 's', ' ', 'B', 'o', 'u', 'r', 'a', 's', '）', '和', '卢', '卡', '雅', '·', '阿', '伊', '纳', '罗', '斯', '托', '（', 'L', 'o', 'u', 'k', 'i', 'a', ' ', 'A', 'i', 'n', 'a', 'r', 'o', 'z', 'i', 'd', 'o', 'u', '）', '夫', '妇', '二', '人', '均', '拥', '有', '希', '腊', '比', '雷', '埃', '夫', '斯', '技', '术', '教', '育', '学', '院', '计', '算', '机', '工', '程', '学', '位', '以', '及', '色', '雷', '斯', '德', '谟', '克', '利', '特', '大', '学', '电', '子', '和', '计', '算', '机', '工', '程', '学', '位', '，', ' ', '都', '从', '事', '过', '软', '件', '开', '发', '工', '作', '，', ' ', '且', '目', '前', '均', '为', '教', '授', '计', '算', '机', '相', '关', '课', '程', '的', '高', '中', '教', '师', '。', '他', '们', '写', '了', '很', '多', '关', '于', '算', '法', '和', '计', '算', '思', '维', '方', '面', '的', '书', '，', ' ', '涉', '及', 'P', 'y', 't', 'h', 'o', 'n', '、', 'C', '#', '、', 'J', 'a', 'v', 'a', '、', 'C', '+', '+', '、', 'P', 'H', 'P', ' ', '和', 'V', ' ', 'i', ' ', 's', ' ', 'u', ' ', 'a', ' ', 'l', ' ', 'B', ' ', 'a', ' ', 's', ' ', 'i', ' ', 'c', ' ', '等', '语', '言', '。']\n"
     ]
    }
   ],
   "source": [
    "# 截取一部分数据便于更快训练\n",
    "dataset.train_data = dataset.train_data[:1000]\n",
    "dataset.test_data = dataset.test_data[:200]\n",
    "\n",
    "dataset.build_vocab(min_freq=0)\n",
    "dataset.convert_tokens_to_ids()\n",
    "print(dataset.train_data[0]['token_ids'])\n",
    "print([dataset.id2token[token_id] for token_id in \\\n",
    "       dataset.train_data[0]['token_ids']])\n",
    "\n",
    "def collect_data(data_split):\n",
    "    X, Y = [], []\n",
    "    for data in data_split:\n",
    "        assert len(data['token_ids']) == len(data['tags'])\n",
    "        X.append(data['token_ids'])\n",
    "        Y.append(data['tags'])\n",
    "    return X, Y\n",
    "\n",
    "train_X, train_Y = collect_data(dataset.train_data)\n",
    "test_X, test_Y = collect_data(dataset.test_data)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7de17192",
   "metadata": {},
   "source": [
    "随后实现隐马尔科夫模型，使用最大似然估计得到模型参数，使用维特比算法进行解码。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "3229fd6a",
   "metadata": {
    "scrolled": false
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "class HMM:\n",
    "    def __init__(self, n_tags, n_tokens):\n",
    "        self.n_tags = n_tags\n",
    "        self.n_tokens = n_tokens\n",
    "    \n",
    "    # 使用最大似然估计计算模型参数\n",
    "    def fit(self, X, Y):\n",
    "        Y0_cnt = np.zeros(self.n_tags)\n",
    "        YY_cnt = np.zeros((self.n_tags, self.n_tags))\n",
    "        YX_cnt = np.zeros((self.n_tags, self.n_tokens))\n",
    "        for x, y in zip(X, Y):\n",
    "            Y0_cnt[y[0]] += 1\n",
    "            last_y = y[0]\n",
    "            for i in range(1, len(y)):\n",
    "                YY_cnt[last_y, y[i]] += 1\n",
    "                last_y = y[i]\n",
    "            for xi, yi in zip(x, y):\n",
    "                YX_cnt[yi, xi] += 1\n",
    "        self.init_prob = Y0_cnt / Y0_cnt.sum()\n",
    "        self.transition_prob = YY_cnt\n",
    "        self.emission_prob = YX_cnt\n",
    "        for i in range(self.n_tags):\n",
    "            # 为了避免训练集过小时除0\n",
    "            yy_sum = YY_cnt[i].sum()\n",
    "            if yy_sum > 0:\n",
    "                self.transition_prob[i] = YY_cnt[i] / yy_sum\n",
    "            yx_sum = YX_cnt[i].sum()\n",
    "            if yx_sum > 0:\n",
    "                self.emission_prob[i] = YX_cnt[i] / yx_sum\n",
    "    \n",
    "    # 已知模型参数的条件下，使用维特比算法解码得到最优标签序列\n",
    "    def viterbi(self, x):\n",
    "        assert hasattr(self, 'init_prob') and hasattr(self,\\\n",
    "            'transition_prob') and hasattr(self, 'emission_prob')\n",
    "        Pi = np.zeros((len(x), self.n_tags))\n",
    "        Y = np.zeros((len(x), self.n_tags), dtype=np.int32)\n",
    "        # 初始化\n",
    "        for i in range(self.n_tags):\n",
    "            Pi[0, i] = self.init_prob[i] * self.emission_prob[i, x[0]]\n",
    "            Y[0, i] = -1\n",
    "        for t in range(1, len(x)):\n",
    "            for i in range(self.n_tags):\n",
    "                tmp = []\n",
    "                for j in range(self.n_tags):\n",
    "                    tmp.append(self.transition_prob[j, i] * Pi[t-1, j])\n",
    "                best_j = np.argmax(tmp)\n",
    "                # 维特比算法递推公式\n",
    "                Pi[t, i] = self.emission_prob[i, x[t]] * tmp[best_j]\n",
    "                Y[t, i] = best_j\n",
    "        y = [np.argmax(Pi[-1])]\n",
    "        for t in range(len(x)-1, 0, -1):\n",
    "            y.append(Y[t, y[-1]])\n",
    "        return np.max(Pi[len(x)-1]), y[::-1]\n",
    "    \n",
    "    def decode(self, X):\n",
    "        Y = []\n",
    "        for x in X:\n",
    "            _, y = self.viterbi(x)\n",
    "            Y.append(y)\n",
    "        return Y\n",
    "\n",
    "hmm = HMM(len(dataset.label2id), len(dataset.token2id))\n",
    "hmm.fit(train_X, train_Y)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "32868ed6",
   "metadata": {},
   "source": [
    "最后验证模型效果"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "4f4b7e88",
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "precision = 0.45762149610217284, recall = 0.3289222699093944, f1 = 0.38274259554692375\n",
      "precision = 0.4189636163175303, recall = 0.23270055113288426, f1 = 0.2992125984251969\n"
     ]
    }
   ],
   "source": [
    "def extract_entity(labels):\n",
    "    entity_list = []\n",
    "    entity_start = -1\n",
    "    entity_length = 0\n",
    "    entity_type = None\n",
    "    for token_index, label in enumerate(labels):\n",
    "        if label.startswith('B'):\n",
    "            if entity_start != -1:\n",
    "                # 遇到了一个新的B，将上一个实体加入列表\n",
    "                entity_list.append((entity_start, entity_length,\\\n",
    "                    entity_type))\n",
    "            # 记录新实体\n",
    "            entity_start = token_index\n",
    "            entity_length = 1\n",
    "            entity_type = label.split('-')[1]\n",
    "        elif label.startswith('I'):\n",
    "            if entity_start != -1:\n",
    "                # 上一个实体未关闭，遇到了一个新的I\n",
    "                if entity_type == label.split('-')[1]:\n",
    "                    # 若上一个实体与当前类型相同，长度+1\n",
    "                    entity_length += 1\n",
    "                else:\n",
    "                    # 若上一个实体与当前类型不同，\n",
    "                    # 将上一个实体加入列表，重置实体\n",
    "                    entity_list.append((entity_start, entity_length,\\\n",
    "                        entity_type))\n",
    "                    entity_start = -1\n",
    "                    entity_length = 0\n",
    "                    entity_type = None\n",
    "        else:\n",
    "            if entity_start != -1:\n",
    "                # 遇到了一个新的O，将上一个实体加入列表\n",
    "                entity_list.append((entity_start, entity_length,\\\n",
    "                    entity_type))\n",
    "            # 重置实体\n",
    "            entity_start = -1\n",
    "            entity_length = 0\n",
    "            entity_type = None\n",
    "    if entity_start != -1:\n",
    "        # 将上一个实体加入列表\n",
    "        entity_list.append((entity_start, entity_length, entity_type))\n",
    "    return entity_list\n",
    "\n",
    "def compute_metric(Y, P):\n",
    "    true_entity_set = set()\n",
    "    pred_entity_set = set()\n",
    "    for sent_no, labels in enumerate(Y):\n",
    "        labels = [dataset.id2label[x] for x in labels]\n",
    "        for ent in extract_entity(labels):\n",
    "            true_entity_set.add((sent_no, ent))\n",
    "    for sent_no, labels in enumerate(P):\n",
    "        labels = [dataset.id2label[x] for x in labels]\n",
    "        for ent in extract_entity(labels):\n",
    "            pred_entity_set.add((sent_no, ent))\n",
    "    if len(true_entity_set) > 0:\n",
    "        recall = len(true_entity_set & pred_entity_set)\\\n",
    "            / len(true_entity_set)\n",
    "    else:\n",
    "        recall = 0\n",
    "    if len(pred_entity_set) > 0:\n",
    "        precision = len(true_entity_set & pred_entity_set)\\\n",
    "            / len(pred_entity_set)\n",
    "    else:\n",
    "        precision = 0\n",
    "    if precision > 0 and recall > 0:\n",
    "        f1 = 2 * precision * recall / (precision + recall)\n",
    "    else:\n",
    "        f1 = 0\n",
    "    return precision, recall, f1\n",
    "\n",
    "train_P = hmm.decode(train_X)\n",
    "p, r, f = compute_metric(train_Y, train_P)\n",
    "print(f'precision = {p}, recall = {r}, f1 = {f}')\n",
    "test_P = hmm.decode(test_X)\n",
    "p, r, f = compute_metric(test_Y, test_P)\n",
    "print(f'precision = {p}, recall = {r}, f1 = {f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3abe8397",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "下面展示基于条件随机场的序列标注模型。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "787f8ad1",
   "metadata": {},
   "outputs": [],
   "source": [
    "\"\"\"\n",
    "代码修改自GitHub项目kmkurn/pytorch-crf\n",
    "（Copyright (c) 2019, Kemal Kurniawan, MIT License（见附录））\n",
    "\"\"\"\n",
    "import torch\n",
    "from torch import nn\n",
    "\n",
    "class CRFLayer(nn.Module):\n",
    "    def __init__(self, n_tags, n_features):\n",
    "        super().__init__()\n",
    "        self.n_tags = n_tags\n",
    "        self.n_features = n_features\n",
    "        # 定义模型参数\n",
    "        self.transitions = nn.Parameter(torch.empty(\\\n",
    "            n_tags, n_tags))\n",
    "        self.emission_weight = nn.Parameter(torch.empty(\\\n",
    "            n_features, n_tags))\n",
    "        self.start_transitions = nn.Parameter(torch.empty(n_tags))\n",
    "        self.end_transitions = nn.Parameter(torch.empty(n_tags))\n",
    "        self.reset_parameters()\n",
    "        \n",
    "    # 使用（-0.1,0.1）之间的均匀分布初始化参数\n",
    "    def reset_parameters(self):\n",
    "        nn.init.uniform_(self.transitions, -0.1, 0.1)\n",
    "        nn.init.uniform_(self.emission_weight, -0.1, 0.1)\n",
    "        nn.init.uniform_(self.start_transitions, -0.1, 0.1)\n",
    "        nn.init.uniform_(self.end_transitions, -0.1, 0.1)\n",
    "    \n",
    "    # 使用动态规划计算得分\n",
    "    def compute_score(self, emissions, tags, masks):\n",
    "        seq_len, batch_size, n_tags = emissions.shape\n",
    "        score = self.start_transitions[tags[0]] + \\\n",
    "            emissions[0, torch.arange(batch_size), tags[0]]\n",
    "        \n",
    "        for i in range(1, seq_len):\n",
    "            score += self.transitions[tags[i-1], tags[i]] * masks[i]\n",
    "            score += emissions[i, torch.arange(batch_size),\\\n",
    "                tags[i]] * masks[i]\n",
    "        \n",
    "        seq_ends = masks.long().sum(dim=0) - 1\n",
    "        last_tags = tags[seq_ends, torch.arange(batch_size)]\n",
    "        score += self.end_transitions[last_tags]\n",
    "        return score\n",
    "    \n",
    "    # 计算配分函数\n",
    "    def computer_normalizer(self, emissions, masks):\n",
    "        seq_len, batch_size, n_tags = emissions.shape\n",
    "        # batch_size * n_tags, [起始分数 + y_0为某标签的发射分数 ...]\n",
    "        score = self.start_transitions + emissions[0]\n",
    "        \n",
    "        for i in range(1, seq_len):\n",
    "            # batch_size * n_tags * 1 [y_{i-1}为某tag的总分]\n",
    "            broadcast_score = score.unsqueeze(2)\n",
    "            # batch_size * 1 * n_tags [y_i为某标签的发射分数]\n",
    "            broadcast_emissions = emissions[i].unsqueeze(1)\n",
    "            # batch_size * n_tags * n_tags [任意y_{i-1}到y_i的总分]\n",
    "            next_score = broadcast_score + self.transitions +\\\n",
    "                broadcast_emissions\n",
    "            # batch_size * n_tags [对y_{i-1}求和]\n",
    "            next_score = torch.logsumexp(next_score, dim=1)\n",
    "            # masks为True则更新，否则保留\n",
    "            score = torch.where(masks[i].unsqueeze(1), next_score, score)\n",
    "            \n",
    "        score += self.end_transitions\n",
    "        return torch.logsumexp(score, dim=1)\n",
    "    \n",
    "    def forward(self, features, tags, masks):\n",
    "        \"\"\"\n",
    "        features: seq_len * batch_size * n_features\n",
    "        tags/masks: seq_len * batch_size\n",
    "        \"\"\"\n",
    "        _, batch_size, _ = features.size()\n",
    "        emissions = torch.matmul(features, self.emission_weight)\n",
    "        masks = masks.to(torch.bool)\n",
    "        \n",
    "        score = self.compute_score(emissions, tags, masks)\n",
    "        partition = self.computer_normalizer(emissions, masks)\n",
    "        \n",
    "        likelihood = score - partition\n",
    "        return likelihood.sum() / batch_size\n",
    "    \n",
    "    def decode(self, features, masks):\n",
    "        # 与computer_normalizer类似，sum变为max\n",
    "        emissions = torch.matmul(features, self.emission_weight)\n",
    "        masks = masks.to(torch.bool)\n",
    "        \n",
    "        seq_len, batch_size, n_tags = emissions.shape\n",
    "        score = self.start_transitions + emissions[0]\n",
    "        history = []\n",
    "        \n",
    "        for i in range(1, seq_len):\n",
    "            broadcast_score = score.unsqueeze(2)\n",
    "            broadcast_emission = emissions[i].unsqueeze(1)\n",
    "            \n",
    "            next_score = broadcast_score + self.transitions +\\\n",
    "                broadcast_emission\n",
    "            next_score, indices = next_score.max(dim=1)\n",
    "            \n",
    "            score = torch.where(masks[i].unsqueeze(1), next_score,\\\n",
    "                score)\n",
    "            history.append(indices)\n",
    "            \n",
    "        score += self.end_transitions\n",
    "        seq_ends = masks.long().sum(dim=0) - 1\n",
    "        best_tags_list = []\n",
    "        \n",
    "        for idx in range(batch_size):\n",
    "            _, best_last_tag = score[idx].max(dim=0)\n",
    "            best_tags = [best_last_tag.item()]\n",
    "            \n",
    "            for hist in reversed(history[:seq_ends[idx]]):\n",
    "                best_last_tag = hist[idx][best_tags[-1]]\n",
    "                best_tags.append(best_last_tag.item())\n",
    "                \n",
    "            best_tags.reverse()\n",
    "            best_tags_list.append(best_tags)\n",
    "            \n",
    "        return best_tags_list"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "69f4af96",
   "metadata": {},
   "outputs": [],
   "source": [
    "class CRF(nn.Module):\n",
    "    def __init__(self, vocab_size, hidden_size, n_tags):\n",
    "        super().__init__()\n",
    "        self.embedding = nn.Embedding(vocab_size, hidden_size)\n",
    "        self.crf = CRFLayer(n_tags, hidden_size)\n",
    "        \n",
    "    def forward(self, input_ids, masks, labels):\n",
    "        \"\"\"\n",
    "        input_ids/masks/labels: batch_size * seq_len\n",
    "        \"\"\"\n",
    "        # 将输入序列转化为词嵌入序列\n",
    "        # batch_size * seq_len * embed_size\n",
    "        embed = self.embedding(input_ids)\n",
    "        embed = torch.transpose(embed, 0, 1)\n",
    "        masks = torch.transpose(masks, 0, 1)\n",
    "        labels = torch.transpose(labels, 0, 1)\n",
    "        # 隐状态和标签、掩码输入条件随机场计算损失\n",
    "        llh = self.crf(embed, labels, masks)\n",
    "        return -llh\n",
    "    \n",
    "    def decode(self, input_ids, masks):\n",
    "        embed = self.embedding(input_ids)\n",
    "        embed = torch.transpose(embed, 0, 1)\n",
    "        masks = torch.transpose(masks, 0, 1)\n",
    "        # 调用CRFLayer进行解码\n",
    "        return self.crf.decode(embed, masks)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "178948b4",
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "epoch-19, loss=48.05: 100%|█| 20/20 [01:27<00:00,  4.38s/it]\n"
     ]
    },
    {
     "data": {
      "image/png": "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OtTl6vdpwo7F6F8y7V3RYY8WLVqY5/QN1N/f3/EGYhcbG2teK4+++uorU/rWfqVTWe16d+/ebYZ2GjZsKLNmzZKhQ4fK8OHD5ZNPPjGv269Jr6+8X++TTz4pt956q3nT02E7DXb691lL+1a71lOV5tr0Vn9xFOXr62v+YVPer1+H6rSH57bbbnNsFmm169VhWT1//f+3JFa53sTERNNnpP2Uffr0kd9++02uv/56M0Slw1XueJ9m13OL0WrHpk2bzL+GrSo+Pl4eeeQRM6ZddPzXygFW/6X38ssvm8caAPTPWPs67r77brGSb775RqZMmSJffPGF+ZfvunXrTNjR3garXSv+pv/Cv/nmm02DtgZ7K9JKxltvvWX+kabVK6u/Z6n+/fubvhzVpk0bWbp0qXnf6tGjh7galR0Leeihh0xD2/z586VGjRqO56tWrSrZ2dmSnJxc7HjtetfXyuObhv7LoV27duZfPfqh/1rQxk69r/8ysNL16sycZs2aFXuuadOmjlkN9ms6dRZDebxeLe/bqzs6S0dL/vpmaa/gWelaT1Waa9Nb/btfVG5urpnBU16v3x509u7da/4BY6/qWO16Fy9ebK5Fh2ns71t6zaNGjZI6depY6npjYmLM9Z3rfcuV79OEHQvQfw1p0NEO/3nz5plpu0W1b9/eDAloL4SdznrQv3RdunSR8qZnz56yceNG869++4dWPnSow37fSterQ5KnLiWgPS21a9c29/XPW98cil5vamqqGeMvb9erM3S0P6EoHx8fx78UrXStpyrNtemt/nLQwG+n/8/rz0d7Ispr0NHp9XPmzDFTkIuy0vVqcNcZSUXft7RiqQFfh6etdL3+/v5mmvnZ3rdc/nupzFue4XJDhw61hYeH2xYsWGA7dOiQ4yM9Pd1xzJAhQ2y1atWyzZs3z/bHH3/YunTpYj6souhsLKtdr85Q8fX1tb300ku2HTt22KZMmWILCgqyff75545jXnnlFVtERITtxx9/tG3YsMHWv39/W926dW0ZGRm28uTuu+82M1WmT59u27Nnj+2HH36wxcTE2B5//HFLXKvOIFy7dq350LffcePGmfv22UelubY+ffrY2rZta1uxYoVtyZIlZkbibbfdZitv15udnW277rrrbDVq1LCtW7eu2HtXVlaW5a63JKfOxipP13viHNeq/+/qbKsPP/zQ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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import numpy as np\n",
    "from torch.utils.data import Dataset, DataLoader\n",
    "from torch.optim import SGD, Adam\n",
    "from copy import deepcopy\n",
    "\n",
    "# 将数据适配为PyTorch数据集\n",
    "class SeqLabelDataset(Dataset):\n",
    "    def __init__(self, tokens, labels):\n",
    "        self.tokens = deepcopy(tokens)\n",
    "        self.labels = deepcopy(labels)\n",
    "        \n",
    "    def __getitem__(self, idx):\n",
    "        return self.tokens[idx], self.labels[idx]\n",
    "    \n",
    "    def __len__(self):\n",
    "        return len(self.labels)\n",
    "    \n",
    "    # 将每个批次转化为PyTorch张量\n",
    "    @classmethod\n",
    "    def collate_batch(cls, inputs):\n",
    "        input_ids, labels, masks = [], [], []\n",
    "        max_len = -1\n",
    "        for ids, tags in inputs:\n",
    "            input_ids.append(ids)\n",
    "            labels.append(tags)\n",
    "            masks.append([1] * len(tags))\n",
    "            max_len = max(max_len, len(ids))\n",
    "        for ids, tags, msks in zip(input_ids, labels, masks):\n",
    "            pad_len = max_len - len(ids)\n",
    "            ids.extend([0] * pad_len)\n",
    "            tags.extend([0] * pad_len)\n",
    "            msks.extend([0] * pad_len)\n",
    "        input_ids = torch.tensor(np.array(input_ids),\\\n",
    "            dtype=torch.long)\n",
    "        labels = torch.tensor(np.array(labels), dtype=torch.long)\n",
    "        masks = torch.tensor(np.array(masks), dtype=torch.uint8)\n",
    "        return {'input_ids': input_ids, 'masks': masks,\\\n",
    "                'labels': labels}\n",
    "    \n",
    "# 准备数据\n",
    "train_dataset = SeqLabelDataset(train_X, train_Y)\n",
    "test_dataset = SeqLabelDataset(test_X, test_Y)\n",
    "\n",
    "from tqdm import tqdm, trange\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "def train(model, batch_size, epochs, learning_rate):\n",
    "    train_dataloader = DataLoader(train_dataset,\n",
    "        batch_size=batch_size, shuffle=True, \n",
    "        collate_fn=SeqLabelDataset.collate_batch)\n",
    "    optimizer = Adam(model.parameters(), lr=learning_rate)\n",
    "    model.zero_grad()\n",
    "    tr_step = []\n",
    "    tr_loss = []\n",
    "    global_step = 0\n",
    "    \n",
    "    with trange(epochs, desc='epoch', ncols=60) as pbar:\n",
    "        for epoch in pbar:\n",
    "            model.train()\n",
    "            for step, batch in enumerate(train_dataloader):\n",
    "                global_step += 1\n",
    "                loss = model(**batch)\n",
    "                loss.backward()\n",
    "                optimizer.step()\n",
    "                model.zero_grad()\n",
    "                pbar.set_description(f'epoch-{epoch}, '+\\\n",
    "                    f'loss={loss.item():.2f}')\n",
    "                \n",
    "                if epoch > 0:\n",
    "                    tr_step.append(global_step)\n",
    "                    tr_loss.append(loss.item())\n",
    "\n",
    "    # 打印损失曲线\n",
    "    plt.plot(tr_step, tr_loss, label='train loss')\n",
    "    plt.xlabel('training steps')\n",
    "    plt.ylabel('loss')\n",
    "    plt.legend()\n",
    "    plt.show()\n",
    "                \n",
    "vocab_size = len(dataset.token2id)\n",
    "hidden_size = 32\n",
    "n_tags = len(dataset.label2id)\n",
    "crf = CRF(vocab_size, hidden_size, n_tags)\n",
    "batch_size = 128\n",
    "epochs = 20\n",
    "learning_rate = 1e-2\n",
    "train(crf, batch_size, epochs, learning_rate)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "4a1e7e25",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "precision = 0.5385982732351448, recall = 0.2528612303290415, f1 = 0.344150576018173\n",
      "precision = 0.4792560801144492, recall = 0.20514390691977955, f1 = 0.28730703259005147\n"
     ]
    }
   ],
   "source": [
    "# 验证效果\n",
    "\n",
    "def evaluate(X, Y, model, batch_size):\n",
    "    dataset = SeqLabelDataset(X, Y)\n",
    "    dataloader = DataLoader(dataset, batch_size=batch_size,\\\n",
    "        collate_fn=SeqLabelDataset.collate_batch)\n",
    "    model.eval()\n",
    "    with torch.no_grad():\n",
    "        P = []\n",
    "        for batch in dataloader:\n",
    "            preds = model.decode(batch['input_ids'], batch['masks'])\n",
    "            P.extend(preds)\n",
    "\n",
    "    p, r, f = compute_metric(Y, P)\n",
    "    print(f'precision = {p}, recall = {r}, f1 = {f}')\n",
    "    \n",
    "evaluate(train_X, train_Y, crf, batch_size)\n",
    "evaluate(test_X, test_Y, crf, batch_size)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cc369a17",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "这里介绍基于双向长短期记忆-条件随机场（BiLSTM-CRF）结构的代码实现。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "990c5bc1",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "from torch import nn\n",
    "\n",
    "# 定义模型，将长短期记忆的输出输入给条件随机场，\n",
    "# 利用条件随机场计算损失和解码\n",
    "class LSTM_CRF(nn.Module):\n",
    "    def __init__(self, vocab_size, hidden_size, num_layers,\\\n",
    "                 dropout, n_tags):\n",
    "        super().__init__()\n",
    "        self.embedding = nn.Embedding(vocab_size, hidden_size)\n",
    "        self.lstm = nn.LSTM(input_size=hidden_size,\n",
    "                            hidden_size=hidden_size,\n",
    "                            num_layers=num_layers,\n",
    "                            batch_first=False,\n",
    "                            dropout=dropout,\n",
    "                            bidirectional=True)\n",
    "        self.crf = CRFLayer(n_tags, hidden_size * 2)\n",
    "    \n",
    "    def forward(self, input_ids, masks, labels):\n",
    "        \"\"\"\n",
    "        input_ids/masks/labels: batch_size * seq_len\n",
    "        \"\"\"\n",
    "        # 将输入序列转化为词嵌入序列\n",
    "        # batch_size * seq_len * embed_size\n",
    "        embed = self.embedding(input_ids)\n",
    "        embed = torch.transpose(embed, 0, 1)\n",
    "        masks = torch.transpose(masks, 0, 1)\n",
    "        labels = torch.transpose(labels, 0, 1)\n",
    "        # 输入长短期记忆得到隐状态\n",
    "        hidden_states, _ = self.lstm(embed)\n",
    "        # 隐状态和标签、掩码输入条件随机场计算损失\n",
    "        llh = self.crf(hidden_states, labels, masks)\n",
    "        return -llh\n",
    "    \n",
    "    def decode(self, input_ids, masks):\n",
    "        # 输入长短期记忆得到隐状态\n",
    "        embed = self.embedding(input_ids)\n",
    "        embed = torch.transpose(embed, 0, 1)\n",
    "        masks = torch.transpose(masks, 0, 1)\n",
    "        hidden_states, _ = self.lstm(embed)\n",
    "        # 调用条件随机场进行解码\n",
    "        return self.crf.decode(hidden_states, masks)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "ab41a9f0",
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "epoch-19, loss=31.36: 100%|█| 20/20 [01:32<00:00,  4.65s/it]\n"
     ]
    },
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# 准备优化器和模型\n",
    "vocab_size = len(dataset.token2id)\n",
    "hidden_size = 32\n",
    "n_layers = 1\n",
    "dropout = 0\n",
    "n_tags = len(dataset.label2id)\n",
    "batch_size = 128\n",
    "epochs = 20\n",
    "learning_rate = 1e-2\n",
    "\n",
    "lstm_crf = LSTM_CRF(vocab_size, hidden_size, n_layers, dropout, n_tags)\n",
    "train(lstm_crf, batch_size, epochs, learning_rate)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "8a9138a5",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "precision = 0.5996969696969697, recall = 0.47186456843109204, f1 = 0.5281558580197492\n",
      "precision = 0.5210696920583469, recall = 0.3937538273116963, f1 = 0.4485524938960586\n"
     ]
    }
   ],
   "source": [
    "evaluate(train_X, train_Y, lstm_crf, batch_size)\n",
    "evaluate(test_X, test_Y, lstm_crf, batch_size)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
